package cinvestment;

import ilog.concert.IloLinearNumExpr;
import ilog.concert.IloNumVar;
import ilog.cplex.IloCplex;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import util.Lang;
import util.MathUtil;
import util.NullOutputStream;

/**
 * CapacityInvestment is programmed to solve the stochastic programming problem
 * based on Bassamboo. And the class implement L-Shaped algorithm
 * **/
public class CapacityInvestment {

	private List<Plant> plants;
	private double[][] demands;

	/**
	 * Number of kinds of products
	 */
	private int N = 4;

	/**
	 * Number of plants
	 */
	private int n;

	/**
	 * Tolerance for the gap between Upper bound and lower bound
	 * **/
	private double epsilon = 0.001;

	/**
	 * Penalty cost for unfilled demand
	 * **/
	private double p = 1.0;

	/**
	 * Basic cost for flexible resource cost
	 * **/
	private double c1 = 0.25;
	private double delta = 0.25;

	/**
	 * Sampling count of demand
	 * **/
	private int simulatedCount = 50;

	/**
	 * iteration counter
	 * **/
	private int iter = 1;

	/**
	 * Store the multiplier of dual problem for each scenario in each iteration
	 * **/
	private List<Double[][]> pi_1;
	private List<Double[][]> pi_2;

	public void lshapedCalculate() {
		this.createDemands();
		this.createPlants();
		this.n = this.plants.size();

		this.pi_1 = new ArrayList<>();
		this.pi_2 = new ArrayList<>();

		double LB = Double.NEGATIVE_INFINITY;
		double UB = Double.MAX_VALUE;

		double probability = BigDecimal
				.valueOf(1.0)
				.divide(BigDecimal.valueOf(simulatedCount), 5,
						BigDecimal.ROUND_HALF_EVEN).doubleValue();

		boolean endFlag = false;
		iter = 1;
		while (!endFlag) {
			/** step 1 **/
			MasterResult mr = this.calculateMasterProblem(probability);

			if (iter > 1 && mr.objValue > LB)
				LB = mr.objValue;

			/** step 2 **/
			double yObj = 0.0;
			Double[][] pi_1_m = new Double[simulatedCount][N];
			Double[][] pi_2_m = new Double[simulatedCount][n];

			double[] capacity = new double[n];
			if (iter == 1) {
				for (int j = 0; j < n; j++)
					capacity[j] = Math.random() * 2;
			} else {
				capacity = Lang.clone(mr.capacities);
			}

			for (int m = 0; m < simulatedCount; m++) {
				double objm = this.calculateSubProblem(capacity, m);
				yObj += objm;

				double[] pi = this.calculateDualProblem(capacity, m);
				for (int i = 0; i < N; i++)
					pi_1_m[m][i] = pi[i];
				for (int j = 0; j < n; j++)
					pi_2_m[m][j] = pi[N + j];
			}
			this.pi_1.add(pi_1_m);
			this.pi_2.add(pi_2_m);

			// calculate phai and update UB
			double phai = 0.0;
			for (int j = 0; j < n; j++) {
				double fixedCost = this.getFixedCost(plants.get(j)
						.getAvailableProductsNumber());
				double flexibleCost = this.getFlexibleCost(plants.get(j)
						.getAvailableProductsNumber());
				phai += fixedCost * mr.openFlag[j] + flexibleCost
						* mr.capacities[j];
			}

			phai += BigDecimal.valueOf(yObj)
					.multiply(BigDecimal.valueOf(probability)).doubleValue();

			if (phai < UB)
				UB = phai;
			System.out.println("LB=" + LB + ";UB=" + UB);

			/** step 3 **/
			if (UB - LB < epsilon) {
				endFlag = true;
				System.out.println("======optimal results iter=" + iter
						+ "======");
				for (int j = 0; j < n; j++) {
					System.out
							.println(mr.openFlag[j] + "\t" + mr.capacities[j]);
				}
			} else {
				iter++;
			}
		}
	}

	/**
	 * Solve the master problem
	 * **/
	private MasterResult calculateMasterProblem(double probability) {
		try {
			IloCplex cplex = new IloCplex();
			cplex.setOut(new NullOutputStream());
			IloNumVar[] K = cplex.numVarArray(n, 0.0, Double.MAX_VALUE);
			IloNumVar[] X = cplex.boolVarArray(n);

			IloLinearNumExpr obj = cplex.linearNumExpr();
			for (int j = 0; j < n; j++) {
				double fixedCost = this.getFixedCost(plants.get(j)
						.getAvailableProductsNumber());
				double flexibleCost = this.getFlexibleCost(plants.get(j)
						.getAvailableProductsNumber());
				obj.addTerm(fixedCost, X[j]);
				obj.addTerm(flexibleCost, K[j]);
			}

			// if there are generated cuts
			if (this.pi_1 != null && this.pi_1.size() > 0) {
				IloNumVar[] theta = cplex.numVarArray(simulatedCount,
						Double.NEGATIVE_INFINITY, Double.MAX_VALUE);
				for (int m = 0; m < simulatedCount; m++)
					obj.addTerm(probability, theta[m]);

				for (int iter = 0; iter < pi_1.size(); iter++) {
					for (int m = 0; m < simulatedCount; m++) {
						IloLinearNumExpr r1 = cplex.linearNumExpr();
						for (int j = 0; j < n; j++) {
							r1.addTerm(-pi_2.get(iter)[m][j], K[j]);
							// System.out.println("@check pi_2 (" + iter + ",m,"
							// + j + ")=" + pi_2.get(iter)[m][j] + "; "
							// + K[j] + "; " + r1);
						}
						double r2 = 0.0;
						for (int i = 0; i < N; i++) {
							r2 += pi_1.get(iter)[m][i] * this.demands[m][i];
							// System.out.println("@check pi_1 (" + iter + ",m,"
							// + i + ")=" + pi_1.get(iter)[m][i]);
						}
						cplex.addGe(theta[m], cplex.sum(r1, r2));
					}
				}
			}

			cplex.addMinimize(obj);

			for (int j = 0; j < n; j++)
				cplex.addGe(cplex.prod(Double.MAX_VALUE, X[j]), K[j]);

			cplex.exportModel(Lang.projectRoot() + "\\data\\master\\master"
					+ iter + ".lp");

			MasterResult mr = new MasterResult();
			if (cplex.solve()) {
				int[] Xv = new int[n];
				for (int j = 0; j < n; j++)
					Xv[j] = (int) cplex.getValue(X[j]);
				double objValue = cplex.getObjValue();
				if (this.pi_1 == null || this.pi_1.size() == 0) {
					objValue += Double.NEGATIVE_INFINITY;
				}
				mr.setValues(objValue, cplex.getValues(K), Xv);
			}
			cplex.end();
			return mr;

		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * Solve the subProblem
	 * 
	 * @return optimal value
	 * **/
	private double calculateSubProblem(double[] Kvalue, int m) {
		double result = 0.0;
		try {
			IloCplex cplex = new IloCplex();
			cplex.setOut(new NullOutputStream());
			IloLinearNumExpr obj = cplex.linearNumExpr();
			IloNumVar[] y = cplex.numVarArray(n * N, 0.0, Double.MAX_VALUE);
			for (int i = 0; i < N; i++) {
				for (int j = 0; j < n; j++) {
					if (plants.get(j).isAvailable(i))
						obj.addTerm(-p, y[i * n + j]);
				}
			}
			cplex.addMinimize(obj);

			for (int i = 0; i < N; i++) {
				IloLinearNumExpr left = cplex.linearNumExpr();
				for (int j = 0; j < n; j++) {
					if (plants.get(j).isAvailable(i))
						left.addTerm(1.0, y[i * n + j]);
				}
				cplex.addLe(left, this.demands[m][i]);
			}
			for (int j = 0; j < n; j++) {
				IloLinearNumExpr left = cplex.linearNumExpr();
				for (int i = 0; i < N; i++) {
					if (plants.get(j).isAvailable(i))
						left.addTerm(1.0, y[i * n + j]);
				}
				cplex.addLe(left, Kvalue[j]);
			}

			cplex.exportModel(Lang.projectRoot()
					+ "\\data\\subproblem\\subproblem" + iter + "_" + m + ".lp");

			if (cplex.solve()) {
				result = cplex.getObjValue();
			}
			cplex.end();

		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * Solve the dual problem
	 * 
	 * @param m
	 *            :scenario
	 * @return a double vector (pi_1,pi2)
	 * **/
	private double[] calculateDualProblem(double[] Kvalue, int m) {
		double[] pi = new double[N + n];
		try {
			IloCplex cplex = new IloCplex();
			// turn off the log output to console
			cplex.setOut(new NullOutputStream());
			IloNumVar[] pi_1_i = cplex.numVarArray(N, Double.NEGATIVE_INFINITY,
					0.0);
			IloNumVar[] pi_2_i = cplex.numVarArray(n, Double.NEGATIVE_INFINITY,
					0.0);

			IloLinearNumExpr obj = cplex.linearNumExpr();
			for (int i = 0; i < N; i++) {
				obj.addTerm(this.demands[m][i], pi_1_i[i]);
			}
			for (int j = 0; j < n; j++)
				obj.addTerm(Kvalue[j], pi_2_i[j]);
			cplex.addMaximize(obj);

			// System.out.println("===demands==");
			// for (int i = 0; i < N; i++)
			// System.out.print(demands[m][i] + "\t");
			// System.out.println();
			// System.out.println("===capacities==");
			// for (int j = 0; j < n; j++)
			// System.out.print(Kvalue[j] + "\t");
			// System.out.println();

			for (int i = 0; i < N; i++) {
				for (int j = 0; j < n; j++) {
					cplex.addLe(cplex.sum(pi_1_i[i], pi_2_i[j]), -p);
				}
			}

			cplex.exportModel(Lang.projectRoot() + "\\data\\dual\\dual" + iter
					+ "_" + m + ".lp");

			if (cplex.solve()) {
				for (int i = 0; i < N; i++)
					pi[i] = cplex.getValue(pi_1_i[i]);
				for (int j = 0; j < n; j++)
					pi[N + j] = cplex.getValue(pi_2_i[j]);

				// System.out.println("@check dual problem======");
				// for (int i = 0; i < n + N; i++)
				// System.out.print(pi[i] + "\t");
				// System.out.println();
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
		return pi;
	}

	private class MasterResult {
		double objValue;
		double[] capacities;
		int[] openFlag;

		void setValues(double value, double[] vars, int[] open) {
			this.objValue = value;
			this.capacities = Lang.clone(vars);
			this.openFlag = Lang.clone(open);
		}
	}

	/**
	 * get fixed cost
	 * 
	 * @param number
	 *            : fixed cost varies based on the number of products a plant
	 *            can produce
	 * */
	private double getFixedCost(int number) {
		// TODO
		return 0.5;
	}

	/**
	 * create a list of plants
	 * **/
	private void createPlants() {
		this.plants = new ArrayList<>();
		int[] set = MathUtil.getSetFromZ(N);
		List<List<Integer>> subSetList = MathUtil.subSet(set);
		for (List<Integer> subList : subSetList) {
			Plant plant = new Plant();
			plant.setAvailableProducts(subList);
			plants.add(plant);
		}
	}

	/**
	 * Create demand data by sampling based on Montacla method
	 * **/
	private void createDemands() {
		this.demands = MathUtil.uniform(simulatedCount, N, 0, 2);
	}

	/**
	 * Calculate the flexible cost for a plant based on the number of products
	 * it can produce. c1[1+(countj-1)*delta]
	 * 
	 * @param number
	 *            : the number of products the plant can produce
	 * **/
	private double getFlexibleCost(int number) {
		return BigDecimal
				.valueOf(c1)
				.multiply(
						BigDecimal.valueOf(1).add(
								BigDecimal.valueOf(delta).multiply(
										BigDecimal.valueOf(number - 1))))
				.doubleValue();
	}
}
